/*
 * java.util.Properties.java modified by Kevin Gaudin to allow usage of enums as keys.
 * 
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.acra;

import android.content.Context;
import org.acra.collector.CrashReportData;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.util.Map;

/**
 * Stores a crash reports data with {@link org.acra.ReportField} enum values as
 * keys. This is basically the source of {@link java.util.Properties} adapted to
 * extend an EnumMap instead of Hashtable and with a few tweaks to avoid losing
 * crazy amounts of android time in the generation of a date comment when
 * storing to file.
 */
final class CrashReportPersister {

	private static final int NONE = 0, SLASH = 1, UNICODE = 2, CONTINUE = 3,
			KEY_DONE = 4, IGNORE = 5;
	private static final String LINE_SEPARATOR = "\n";

	private final Context context;

	CrashReportPersister(Context context) {
		this.context = context;
	}

	/**
	 * Loads properties from the specified {@code InputStream}. The encoding is
	 * ISO8859-1.
	 * 
	 * @param fileName
	 *            Name of the report file from which to load the CrashData.
	 * @return CrashReportData read from the supplied InputStream.
	 * @throws java.io.IOException
	 *             if error occurs during reading from the {@code InputStream}.
	 */
	public CrashReportData load(String fileName) throws IOException {

		final FileInputStream in = context.openFileInput(fileName);
		if (in == null) {
			throw new IllegalArgumentException(
					"Invalid crash report fileName : " + fileName);
		}

		try {
			final BufferedInputStream bis = new BufferedInputStream(in,
					ACRAConstants.DEFAULT_BUFFER_SIZE_IN_BYTES);
			bis.mark(Integer.MAX_VALUE);
			final boolean isEbcdic = isEbcdic(bis);
			bis.reset();

			if (!isEbcdic) {
				return load(new InputStreamReader(bis, "ISO8859-1")); //$NON-NLS-1$
			} else {
				return load(new InputStreamReader(bis)); //$NON-NLS-1$
			}
		} finally {
			in.close();
		}
	}

	/**
	 * Stores the mappings in this Properties to the specified OutputStream,
	 * putting the specified comment at the beginning. The output from this
	 * method is suitable for being read by the load() method.
	 * 
	 * @param crashData
	 *            CrashReportData to save.
	 * @param fileName
	 *            Name of the file to which to store the CrashReportData.
	 * @throws java.io.IOException
	 *             if the CrashReportData could not be written to the
	 *             OutputStream.
	 */
	public void store(CrashReportData crashData, String fileName)
			throws IOException {

		final OutputStream out = context.openFileOutput(fileName,
				Context.MODE_PRIVATE);
		try {
			final StringBuilder buffer = new StringBuilder(200);
			final OutputStreamWriter writer = new OutputStreamWriter(out,
					"ISO8859_1"); //$NON-NLS-1$

			for (final Map.Entry<ReportField, String> entry : crashData
					.entrySet()) {
				final String key = entry.getKey().toString();
				dumpString(buffer, key, true);
				buffer.append('=');
				dumpString(buffer, entry.getValue(), false);
				buffer.append(LINE_SEPARATOR);
				writer.write(buffer.toString());
				buffer.setLength(0);
			}
			writer.flush();
		} finally {
			out.close();
		}
	}

	private boolean isEbcdic(BufferedInputStream in) throws IOException {
		byte b;
		while ((b = (byte) in.read()) != -1) {
			if (b == 0x23 || b == 0x0a || b == 0x3d) {// ascii: newline/#/=
				return false;
			}
			if (b == 0x15) {// EBCDIC newline
				return true;
			}
		}
		// we found no ascii newline, '#', neither '=', relative safe to
		// consider it
		// as non-ascii, the only exception will be a single line with only
		// key(no value and '=')
		// in this case, it should be no harm to read it in default charset
		return false;
	}

	/**
	 * Loads properties from the specified InputStream. The properties are of
	 * the form <code>key=value</code>, one property per line. It may be not
	 * encode as 'ISO-8859-1'.The {@code Properties} file is interpreted
	 * according to the following rules:
	 * <ul>
	 * <li>Empty lines are ignored.</li>
	 * <li>Lines starting with either a "#" or a "!" are comment lines and are
	 * ignored.</li>
	 * <li>A backslash at the end of the line escapes the following newline
	 * character ("\r", "\n", "\r\n"). If there's a whitespace after the
	 * backslash it will just escape that whitespace instead of concatenating
	 * the lines. This does not apply to comment lines.</li>
	 * <li>A property line consists of the key, the space between the key and
	 * the value, and the value. The key goes up to the first whitespace, "=" or
	 * ":" that is not escaped. The space between the key and the value contains
	 * either one whitespace, one "=" or one ":" and any number of additional
	 * whitespaces before and after that character. The value starts with the
	 * first character after the space between the key and the value.</li>
	 * <li>Following escape sequences are recognized: "\ ", "\\", "\r", "\n",
	 * "\!", "\#", "\t", "\b", "\f", and "&#92;uXXXX" (unicode character).</li>
	 * </ul>
	 * 
	 * @param reader
	 *            Reader from which to read the properties of this
	 *            CrashReportData.
	 * @return CrashReportData read from the supplied Reader.
	 * @throws java.io.IOException
	 *             if the properties could not be read.
	 * @since 1.6
	 */
	private synchronized CrashReportData load(Reader reader) throws IOException {
		int mode = NONE, unicode = 0, count = 0;
		char nextChar, buf[] = new char[40];
		int offset = 0, keyLength = -1, intVal;
		boolean firstChar = true;

		final CrashReportData crashData = new CrashReportData();
		final BufferedReader br = new BufferedReader(reader,
				ACRAConstants.DEFAULT_BUFFER_SIZE_IN_BYTES);

		while (true) {
			intVal = br.read();
			if (intVal == -1) {
				break;
			}
			nextChar = (char) intVal;

			if (offset == buf.length) {
				final char[] newBuf = new char[buf.length * 2];
				System.arraycopy(buf, 0, newBuf, 0, offset);
				buf = newBuf;
			}
			if (mode == UNICODE) {
				final int digit = Character.digit(nextChar, 16);
				if (digit >= 0) {
					unicode = (unicode << 4) + digit;
					if (++count < 4) {
						continue;
					}
				} else if (count <= 4) {
					// luni.09=Invalid Unicode sequence: illegal character
					throw new IllegalArgumentException("luni.09");
				}
				mode = NONE;
				buf[offset++] = (char) unicode;
				if (nextChar != '\n' && nextChar != '\u0085') {
					continue;
				}
			}
			if (mode == SLASH) {
				mode = NONE;
				switch (nextChar) {
				case '\r':
					mode = CONTINUE; // Look for a following \n
					continue;
				case '\u0085':
				case '\n':
					mode = IGNORE; // Ignore whitespace on the next line
					continue;
				case 'b':
					nextChar = '\b';
					break;
				case 'f':
					nextChar = '\f';
					break;
				case 'n':
					nextChar = '\n';
					break;
				case 'r':
					nextChar = '\r';
					break;
				case 't':
					nextChar = '\t';
					break;
				case 'u':
					mode = UNICODE;
					unicode = count = 0;
					continue;
				}
			} else {
				switch (nextChar) {
				case '#':
				case '!':
					if (firstChar) {
						while (true) {
							intVal = br.read();
							if (intVal == -1) {
								break;
							}
							nextChar = (char) intVal; // & 0xff
														// not
														// required
							if (nextChar == '\r' || nextChar == '\n'
									|| nextChar == '\u0085') {
								break;
							}
						}
						continue;
					}
					break;
				case '\n':
					if (mode == CONTINUE) { // Part of a \r\n sequence
						mode = IGNORE; // Ignore whitespace on the next line
						continue;
					}
					// fall into the next case
				case '\u0085':
				case '\r':
					mode = NONE;
					firstChar = true;
					if (offset > 0 || (offset == 0 && keyLength == 0)) {
						if (keyLength == -1) {
							keyLength = offset;
						}
						final String temp = new String(buf, 0, offset);
						crashData.put(
								Enum.valueOf(ReportField.class,
										temp.substring(0, keyLength)),
								temp.substring(keyLength));
					}
					keyLength = -1;
					offset = 0;
					continue;
				case '\\':
					if (mode == KEY_DONE) {
						keyLength = offset;
					}
					mode = SLASH;
					continue;
				case ':':
				case '=':
					if (keyLength == -1) { // if parsing the key
						mode = NONE;
						keyLength = offset;
						continue;
					}
					break;
				}
				if (Character.isWhitespace(nextChar)) {
					if (mode == CONTINUE) {
						mode = IGNORE;
					}
					// if key length == 0 or value length == 0
					if (offset == 0 || offset == keyLength || mode == IGNORE) {
						continue;
					}
					if (keyLength == -1) { // if parsing the key
						mode = KEY_DONE;
						continue;
					}
				}
				if (mode == IGNORE || mode == CONTINUE) {
					mode = NONE;
				}
			}
			firstChar = false;
			if (mode == KEY_DONE) {
				keyLength = offset;
				mode = NONE;
			}
			buf[offset++] = nextChar;
		}
		if (mode == UNICODE && count <= 4) {
			// luni.08=Invalid Unicode sequence: expected format \\uxxxx
			throw new IllegalArgumentException("luni.08");
		}
		if (keyLength == -1 && offset > 0) {
			keyLength = offset;
		}
		if (keyLength >= 0) {
			final String temp = new String(buf, 0, offset);
			final ReportField key = Enum.valueOf(ReportField.class,
					temp.substring(0, keyLength));
			String value = temp.substring(keyLength);
			if (mode == SLASH) {
				value += "\u0000";
			}
			crashData.put(key, value);
		}

		return crashData;
	}

	/**
	 * Constructs a new {@code Properties} object.
	 * 
	 * @param buffer
	 *            StringBuilder to populate with the supplied property.
	 * @param string
	 *            String to append to the buffer.
	 * @param key
	 *            Whether the String is a key value or not.
	 */
	private void dumpString(StringBuilder buffer, String string, boolean key) {
		int i = 0;
		if (!key && i < string.length() && string.charAt(i) == ' ') {
			buffer.append("\\ "); //$NON-NLS-1$
			i++;
		}

		for (; i < string.length(); i++) {
			char ch = string.charAt(i);
			switch (ch) {
			case '\t':
				buffer.append("\\t"); //$NON-NLS-1$
				break;
			case '\n':
				buffer.append("\\n"); //$NON-NLS-1$
				break;
			case '\f':
				buffer.append("\\f"); //$NON-NLS-1$
				break;
			case '\r':
				buffer.append("\\r"); //$NON-NLS-1$
				break;
			default:
				if ("\\#!=:".indexOf(ch) >= 0 || (key && ch == ' ')) {
					buffer.append('\\');
				}
				if (ch >= ' ' && ch <= '~') {
					buffer.append(ch);
				} else {
					final String hex = Integer.toHexString(ch);
					buffer.append("\\u"); //$NON-NLS-1$
					for (int j = 0; j < 4 - hex.length(); j++) {
						buffer.append("0"); //$NON-NLS-1$
					}
					buffer.append(hex);
				}
			}
		}
	}
}
